﻿Imports System.Runtime.CompilerServices
Imports System.Reflection
Imports System.Text.RegularExpressions
Imports System.Globalization

''' <summary>
''' プロパティ拡張メソッド
''' </summary>
''' <remarks></remarks>
Public Module PropertyInfoExtension

    ''' <summary>
    ''' キーかどうかを示します
    ''' </summary>
    ''' <param name="prop"></param>
    ''' <returns></returns>
    ''' <remarks>KeyConstraintを参照しています</remarks>
    <Extension()>
    Public Function IsKey(prop As PropertyInfo) As Boolean
        If prop Is Nothing Then Throw New ArgumentNullException("prop")

        '#120 LoadableListActivatorが遅い
        'キー判定は多用するのでキャッシュします
        Dim logic =
            Function() As Boolean
                'キー属性がある場合、優先して採用する
                If prop.GetCustomAttributes(GetType(KeyAttribute), False).Any Then Return True

                '命名規約に沿っている場合、キー判定
                If prop.IsLiteralKey Then Return True
                If prop.IsNRelationKey Then Return True

                'それ以外はキーではない
                Return False
            End Function

        Static cache As New Dictionary(Of PropertyInfo, Boolean)
        If cache.ContainsKey(prop) = False Then
            cache.Add(prop, logic.Invoke())
        End If

        Return cache(prop)
    End Function

    ''' <summary>
    ''' キー命名規則にあっているかを示します
    ''' </summary>
    ''' <param name="prop"></param>
    ''' <returns></returns>
    ''' <remarks>KeyConstraintを参照しています</remarks>
    <Extension()>
    Public Function IsLiteralKey(prop As MemberInfo) As Boolean
        If prop Is Nothing Then Throw New ArgumentNullException("prop")

        Dim pm = Regex.Match(prop.Name, KeyConstraint.Instance.KeyPattern, RegexOptions.IgnoreCase)

        'プロパティ名が命名規約と異なる
        If pm.Success = False Then Return False

        '接頭語（自クラス名）が命名規約と異なる
        Dim prefix = pm.Groups("myclass").Value
        If String.IsNullOrEmpty(prefix) = False AndAlso prefix.Equals(prop.DeclaringType.Name) = False Then Return False

        'キーと判断する
        Return True
    End Function

    ''' <summary>
    ''' Nリレーションキー命名規約にあっているかを示します
    ''' </summary>
    ''' <param name="prop"></param>
    ''' <returns></returns>
    ''' <remarks>KeyConstraintを参照しています</remarks>
    <Extension()>
    Public Function IsNRelationKey(prop As PropertyInfo) As Boolean
        If prop Is Nothing Then Throw New ArgumentNullException("prop")

        'Nリレーション判定
        Dim m As Match = Regex.Match(prop.DeclaringType.Name, KeyConstraint.Instance.NRelationClassPattern)

        'クラス名が命名規約とは異なる
        If m.Success = False Then Return False

        Dim names As String() = {m.Groups("parent1").Value, m.Groups("parent2").Value}

        '検査対象が命名規約のプロパティではない場合
        If (From el In names Where el.Equals(prop.Name)).Any = False Then Return False

        '命名規約にあったプロパティが存在しない場合
        If (From el In names Where prop.DeclaringType.GetProperty(el) Is Nothing).Any Then Return False

        'キーと判断する
        Return True
    End Function

    ''' <summary>
    ''' プロパティの型がリテラルかどうかを示します
    ''' </summary>
    ''' <param name="source"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    <Extension()>
    Public Function IsLiteralValue(source As PropertyInfo) As Boolean
        Return If(source.ToValueType = ValueTypes.Literal, True, False)
    End Function

    ''' <summary>
    ''' プロパティの型がオブジェクトかどうかを示します
    ''' </summary>
    ''' <param name="source"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    <Extension()>
    Public Function IsObjectValue(source As PropertyInfo) As Boolean
        Return If(source.ToValueType = ValueTypes.Object, True, False)
    End Function

    ''' <summary>
    ''' プロパティの型がコレクションかどうかを示します
    ''' </summary>
    ''' <param name="source"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    <Extension()>
    Public Function IsCollectiontValue(source As PropertyInfo) As Boolean
        Return If(source.ToValueType = ValueTypes.Collection, True, False)
    End Function

    ''' <summary>
    ''' 値タイプを返します
    ''' </summary>
    ''' <param name="prop"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    <Extension()>
    Public Function ToValueType(prop As PropertyInfo) As ValueTypes
        If prop Is Nothing Then Throw New ArgumentNullException("prop")

        Dim pt As Type = prop.PropertyType

        'String型
        If pt.Equals(GetType(String)) Then Return ValueTypes.Literal

        'バイト配列
        If pt.Equals(GetType(Byte())) Then Return ValueTypes.Literal

        '列挙体
        If pt.IsEnum Then Return ValueTypes.Literal

        If pt.GetInterface(GetType(IEnumerable).FullName) Is Nothing Then
            'IEnumerableインターフェイスを実装していない場合
            If pt.FullName.StartsWith("System.") Then
                Return ValueTypes.Literal
            Else
                Return ValueTypes.[Object]
            End If
        Else
            'IEnumerableインターフェイスを実装している場合
            If pt.IsGenericType AndAlso
                pt.GetGenericArguments.Count = 1 AndAlso
                pt.GetGenericArguments.First.FullName.StartsWith("System.") = False Then
                Return ValueTypes.Collection
            End If
        End If

        Return ValueTypes.Unknown
    End Function

    ''' <summary>
    ''' リテラルプロパティに変換します
    ''' </summary>
    ''' <param name="source"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    <Extension()>
    Public Function ToLiteralProperties(source As PropertyInfo) As IEnumerable(Of LiteralProperty)
        If source Is Nothing Then Throw New ArgumentNullException("source")

        Static caches As New Dictionary(Of PropertyInfo, IEnumerable(Of LiteralProperty))
        If caches.ContainsKey(source) Then Return caches(source)

        'リテラル値の場合
        If source.IsLiteralValue Then
            Dim c = {LiteralProperty.Create(source)}
            caches.Add(source, c)
            Return c
        End If

        'オブジェクト値の場合
        If source.IsObjectValue Then
            Dim lst As New List(Of RelatedProperty)
            caches.Add(source, lst)

            Dim pt = source.PropertyType
            'upperのキープロパティを取得
            For Each kprop In pt.ToEntityDefinition.KeyProperties
                'キープロパティをリテラルプロパティに変換
                For Each lprop In kprop.ToLiteralProperties
                    Dim c = RelatedProperty.Create(source, lprop)
                    lst.Add(c)
                Next
            Next
            Return lst
        End If

        'それ以外はなし
        Return Enumerable.Empty(Of LiteralProperty)()
    End Function


    ''' <summary>
    ''' プロパティの型を取得します
    ''' Nullableの場合、格納できる型が返されます
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks></remarks>
    <Extension()>
    Public Function GetActualPropertyType(prop As PropertyInfo) As Type
        If prop Is Nothing Then Throw New ArgumentNullException("prop")

        Dim t As Type = Nothing

        If prop.PropertyType.IsGenericType Then
            'Nullableの場合、格納できる型をセット
            t = prop.PropertyType.GetGenericArguments(0)
        Else
            t = prop.PropertyType
        End If

        Return t
    End Function
End Module